package org.opentripplanner.graph_builder.module;

import com.conveyal.gtfs.GTFSFeed;
import com.conveyal.gtfs.model.*;
import org.mapdb.Fun;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.graph_builder.linking.SimpleStreetSplitter;
import org.opentripplanner.graph_builder.model.GtfsBundle;
import org.opentripplanner.graph_builder.module.osm.DefaultWayPropertySetSource;
import org.opentripplanner.graph_builder.module.osm.OpenStreetMapModule;
import org.opentripplanner.openstreetmap.impl.AnyFileBasedOpenStreetMapProviderImpl;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.vertextype.TransitStop;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Get fake graphs.
 */
public class FakeGraph {
    /** frequency of trips generated by addTransit, seconds */
    public static final int FREQUENCY = 600;

    /** Travel time between stops of trips generated by addTransit, seconds */
    public static final int TRAVEL_TIME = 500;

    /** Build a graph in Columbus, OH with no transit */
    public static Graph buildGraphNoTransit () throws UnsupportedEncodingException {
        Graph gg = new Graph();

        OpenStreetMapModule loader = new OpenStreetMapModule();
        loader.setDefaultWayPropertySetSource(new DefaultWayPropertySetSource());
        AnyFileBasedOpenStreetMapProviderImpl provider = new AnyFileBasedOpenStreetMapProviderImpl();

        File file = new File(
                URLDecoder.decode(FakeGraph.class.getResource("columbus.osm.pbf").getFile(),
                        "UTF-8"));

        provider.setPath(file);
        loader.setProvider(provider);

        loader.buildGraph(gg, new HashMap<Class<?>, Object>());
        return gg;
    }

    /** Add transit (not just stops) to a Columbus graph */
    public static void addTransit (Graph gg) throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = new GTFSFeed();
        Agency a = createDummyAgency("agency", "Agency", "America/New_York");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency = a;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = createDummyService();
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.2182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service = s;
            t.route = r;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
        }

        File tempFile = File.createTempFile("gtfs", ".zip");
        feed.toFile(tempFile.getAbsolutePath());

        // phew. load it into the graph.
        GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile)));
        gtfs.buildGraph(gg, new HashMap<>());
    }

    /** Add many transit lines to a lot of stops */
    public static void addTransitMultipleLines (Graph g) throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = new GTFSFeed();
        Agency a = createDummyAgency("agency", "Agency", "America/New_York");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency = a;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = createDummyService();
        feed.services.put(s.service_id, s);

        int stopIdx = 0;
        while (stopIdx < 10000) {
            com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
            s1.stop_id = s1.stop_name = "s" + stopIdx++;
            s1.stop_lat = 39.9354 + (stopIdx % 100) * 1e-3;
            s1.stop_lon = -83.0589 + (stopIdx / 100) * 1e-3;
            feed.stops.put(s1.stop_id, s1);

            com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
            s2.stop_id = s2.stop_name = "s" + stopIdx++;
            s2.stop_lat = 39.9354 + (stopIdx % 100) * 1e-3;
            s2.stop_lon = -83.0589 + (stopIdx / 100) * 1e-3;
            feed.stops.put(s2.stop_id, s2);

            // make timetabled trips
            int departure = 8 * 3600;
            Trip t = new Trip();
            t.trip_id = "trip" + departure + "_" + stopIdx;
            t.service = s;
            t.route = r;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + 500;
            st2.departure_time = departure + 500;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);

        }

        File tempFile = File.createTempFile("gtfs", ".zip");
        feed.toFile(tempFile.getAbsolutePath());

        // phew. load it into the graph.
        GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile)));
        gtfs.buildGraph(g, new HashMap<>());
    }

    public static void addPerpendicularRoutes(Graph graph) throws Exception {
        GTFSFeed feed = new GTFSFeed();
        Agency agencyA = createDummyAgency("agencyA", "Agency A", "America/New_York");
        feed.agency.put("agencyA", agencyA);
        Agency agencyB = createDummyAgency("agencyB", "Agency B", "America/New_York");
        feed.agency.put("agencyB", agencyB);
        Service s = createDummyService();
        feed.services.put(s.service_id, s);

        int stopIdX = 0;
        int stopIdY = 0;
        for (double lat = 39.9058; lat < 40.0281; lat += 0.005) {
            stopIdX = 0;
            for (double lon = -83.1341; lon < -82.8646; lon += 0.005) {
                com.conveyal.gtfs.model.Stop stop = new com.conveyal.gtfs.model.Stop();
                stop.stop_id = stop.stop_name = String.format("s-%d-%d", stopIdX, stopIdY);
                stop.stop_lat = lat;
                stop.stop_lon = lon;
                feed.stops.put(stop.stop_id, stop);
                stopIdX++;
            }
            stopIdY++;
        }

        for (int i = 0; i < stopIdY; i++) {
            Route route = new Route();
            route.route_short_name = "hr" + i;
            route.route_long_name = i + "th Horizontal Street";
            route.route_type = Route.BUS;
            route.agency = agencyA;
            route.route_id = "horizontalroute" + i;
            feed.routes.put(route.route_id, route);
        }
        for (int i = 0; i < stopIdX; i++) {
            Route route = new Route();
            route.route_short_name = "vr" + i;
            route.route_long_name = i + "th Vertical Street";
            route.route_type = Route.TRAM;
            route.agency = agencyB;
            route.route_id = "verticalroute" + i;
            feed.routes.put(route.route_id, route);
        }

        Map<String, Route> routes = feed.routes;
        com.conveyal.gtfs.model.Stop stop;
        for (Route route : routes.values()) {
            int routeId = Integer.parseInt(route.route_short_name.substring(2));
            int x, y;
            boolean isHorizontal = route.route_short_name.startsWith("hr");
            for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
                Trip t = new Trip();
                t.trip_id = "trip:" + route.route_id + ":" + departure;
                t.service = s;
                t.route = route;
                feed.trips.put(t.trip_id, t);

                int departureTime = departure;
                int nrOfStops = (isHorizontal ? stopIdX : stopIdY);
                for (int stopSequenceNr = 0; stopSequenceNr < nrOfStops; stopSequenceNr++) {
                    x = (isHorizontal ? stopSequenceNr : routeId);
                    y = (isHorizontal ? routeId : stopSequenceNr);
                    stop = feed.stops.get(String.format("s-%d-%d", x, y));
                    StopTime st1 = new StopTime();
                    st1.trip_id = t.trip_id;
                    st1.arrival_time = departureTime;
                    st1.departure_time = departureTime;
                    st1.stop_id = stop.stop_id;
                    st1.stop_sequence = stopSequenceNr + 1;
                    feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);
                    //connect last stop to first so graph is fully reachable
                    if (stopSequenceNr == 0) {
                        StopTime stopTime = new StopTime();
                        stopTime.trip_id = t.trip_id;
                        stopTime.arrival_time = departureTime + nrOfStops * 120 + 30 * 60;
                        stopTime.departure_time = departureTime + nrOfStops * 120 + 30 * 60;
                        stopTime.stop_id = stop.stop_id;
                        stopTime.stop_sequence = stopSequenceNr + 1 + nrOfStops;
                        feed.stop_times.put(new Fun.Tuple2(stopTime.trip_id, stopTime.stop_sequence), stopTime);
                    }
                    departureTime += 120;
                }
            }
        }
        File tempFile = File.createTempFile("gtfs", ".zip");
        feed.toFile(tempFile.getAbsolutePath());

        // phew. load it into the graph.
        GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile)));
        gtfs.buildGraph(graph, new HashMap<>());
    }

    private static Service createDummyService() {
        Service s = new Service("service");
        s.calendar = new Calendar();
        s.calendar.service = s;
        s.calendar.monday = s.calendar.tuesday = s.calendar.wednesday = s.calendar.thursday = s.calendar.friday =
                s.calendar.saturday = s.calendar.sunday = 1;
        s.calendar.start_date = 19991231;
        s.calendar.end_date = 21001231;
        return s;
    }

    private static Agency createDummyAgency(String id, String name, String timeZone) {
        Agency a = new Agency();
        a.agency_id = id;
        a.agency_name = name;
        a.agency_timezone = timeZone;
        try {
            a.agency_url = new URL("http://www.example.com/" + id);
        } catch (MalformedURLException e) {
            // This really can't happen
            assert false;
            a.agency_url = null;
        }
        return a;
    }

    /** Add a transit line with multiple patterns to a Columbus graph. Most trips serve stops s1, s2, s3 but some serve only s1, s3 */
    public static void addMultiplePatterns (Graph gg) throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = new GTFSFeed();
        Agency a = createDummyAgency("agency", "Agency", "America/New_York");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency = a;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = createDummyService();
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.2182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        com.conveyal.gtfs.model.Stop s3 = new com.conveyal.gtfs.model.Stop();
        s3.stop_id = s3.stop_name = "s3";
        s3.stop_lat = 39.9510;
        s3.stop_lon = -83.0007;
        feed.stops.put(s3.stop_id, s3);

        // make timetabled trips
        for (int departure = 7 * 3600, dcount = 0; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service = s;
            t.route = r;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            // occasionally skip second stop
            boolean secondStop = dcount++ % 10 != 0;
            if (secondStop) {
                StopTime st2 = new StopTime();
                st2.trip_id = t.trip_id;
                st2.arrival_time = departure + TRAVEL_TIME;
                st2.departure_time = departure + TRAVEL_TIME;
                st2.stop_sequence = 2;
                st2.stop_id = s2.stop_id;
                feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
            }

            StopTime st3 = new StopTime();
            st3.trip_id = t.trip_id;
            st3.arrival_time = departure + (secondStop ? 2 : 1) * TRAVEL_TIME;
            st3.departure_time = departure + (secondStop ? 2 : 1) * TRAVEL_TIME;
            st3.stop_sequence = 3;
            st3.stop_id = s3.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st3.trip_id, st3.stop_sequence), st3);
        }

        File tempFile = File.createTempFile("gtfs", ".zip");
        feed.toFile(tempFile.getAbsolutePath());

        // phew. load it into the graph.
        GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile)));
        gtfs.buildGraph(gg, new HashMap<>());
    }

    /** Add a regular grid of stops to the graph */
    public static void addRegularStopGrid(Graph g) {
        int count = 0;
        for (double lat = 39.9058; lat < 40.0281; lat += 0.005) {
            for (double lon = -83.1341; lon < -82.8646; lon += 0.005) {
                String id = "" + count++;
                AgencyAndId aid = new AgencyAndId("TEST", id);
                Stop stop = new Stop();
                stop.setLat(lat);
                stop.setLon(lon);
                stop.setName(id);
                stop.setCode(id);
                stop.setId(aid);

                new TransitStop(g, stop);
                count++;
            }
        }
    }

    /** add some extra stops to the graph */
    public static void addExtraStops (Graph g) {
        int count = 0;
        double lon = -83;
        for (double lat = 40; lat < 40.01; lat += 0.005) {
            String id = "EXTRA_" + count++;
            AgencyAndId aid = new AgencyAndId("EXTRA", id);
            Stop stop = new Stop();
            stop.setLat(lat);
            stop.setLon(lon);
            stop.setName(id);
            stop.setCode(id);
            stop.setId(aid);

            new TransitStop(g, stop);
            count++;
        }

        // add some duplicate stops
        lon = -83.1341 + 0.1;

        for (double lat = 39.9058; lat < 40.0281; lat += 0.005) {
            String id = "" + count++;
            AgencyAndId aid = new AgencyAndId("EXTRA", id);
            Stop stop = new Stop();
            stop.setLat(lat);
            stop.setLon(lon);
            stop.setName(id);
            stop.setCode(id);
            stop.setId(aid);

            new TransitStop(g, stop);
            count++;
        }

        // add some almost duplicate stops
        lon = -83.1341 + 0.15;

        for (double lat = 39.9059; lat < 40.0281; lat += 0.005) {
            String id = "" + count++;
            AgencyAndId aid = new AgencyAndId("EXTRA", id);
            Stop stop = new Stop();
            stop.setLat(lat);
            stop.setLon(lon);
            stop.setName(id);
            stop.setCode(id);
            stop.setId(aid);

            new TransitStop(g, stop);
            count++;
        }
    }

    /** Add transit (in both directions) to a Columbus graph */
    public static void addTransitBidirectional (Graph gg) throws Exception {
        // using conveyal GTFS lib to build GTFS so a lot of code does not have to be rewritten later
        // once we're using the conveyal GTFS lib for everything we ought to be able to do this
        // without even writing out the GTFS to a file.
        GTFSFeed feed = new GTFSFeed();
        Agency a = createDummyAgency("agency", "Agency", "America/New_York");
        feed.agency.put("agency", a);

        Route r = new Route();
        r.route_short_name = "1";
        r.route_long_name = "High Street";
        r.route_type = 3;
        r.agency = a;
        r.route_id = "route";
        feed.routes.put(r.route_id, r);

        Service s = createDummyService();
        feed.services.put(s.service_id, s);

        com.conveyal.gtfs.model.Stop s1 = new com.conveyal.gtfs.model.Stop();
        s1.stop_id = s1.stop_name = "s1";
        s1.stop_lat = 40.2182;
        s1.stop_lon = -83.0889;
        feed.stops.put(s1.stop_id, s1);

        com.conveyal.gtfs.model.Stop s2 = new com.conveyal.gtfs.model.Stop();
        s2.stop_id = s2.stop_name = "s2";
        s2.stop_lat = 39.9621;
        s2.stop_lon = -83.0007;
        feed.stops.put(s2.stop_id, s2);

        // make timetabled trips
        for (int departure = 7 * 3600; departure < 20 * 3600; departure += FREQUENCY) {
            Trip t = new Trip();
            t.trip_id = "trip" + departure;
            t.service = s;
            t.route = r;
            t.direction_id = 0;
            feed.trips.put(t.trip_id, t);

            StopTime st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s1.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            StopTime st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s2.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);

            // opposite direction
            t = new Trip();
            t.trip_id = "trip_back" + departure;
            t.service = s;
            t.route = r;
            t.direction_id = 1;
            feed.trips.put(t.trip_id, t);

            st1 = new StopTime();
            st1.trip_id = t.trip_id;
            st1.arrival_time = departure;
            st1.departure_time = departure;
            st1.stop_id = s2.stop_id;
            st1.stop_sequence = 1;
            feed.stop_times.put(new Fun.Tuple2(st1.trip_id, st1.stop_sequence), st1);

            st2 = new StopTime();
            st2.trip_id = t.trip_id;
            st2.arrival_time = departure + TRAVEL_TIME;
            st2.departure_time = departure + TRAVEL_TIME;
            st2.stop_sequence = 2;
            st2.stop_id = s1.stop_id;
            feed.stop_times.put(new Fun.Tuple2(st2.trip_id, st2.stop_sequence), st2);
        }

        File tempFile = File.createTempFile("gtfs", ".zip");
        feed.toFile(tempFile.getAbsolutePath());

        // phew. load it into the graph.
        GtfsModule gtfs = new GtfsModule(Arrays.asList(new GtfsBundle(tempFile)));
        gtfs.buildGraph(gg, new HashMap<>());
    }

    /** link the stops in the graph */
    public static void link (Graph g) {
        SimpleStreetSplitter linker = new SimpleStreetSplitter(g);
        linker.link();
    }

}
